[L3H]easy_android

~~ 题目实际上不难,找到关键词跟进甚至不用管他的逻辑和架构丢给ai一把梭~~

基本操作:放进jadx中,跟进逻辑,有load加载native方法,吧lib文件中的so文件用ida打开查看逻辑

java层:

先是查看主类,发现是主类直接继承了TauriActivity这个类,疑似使用了tauri框架。(都学过java,别告诉我不知道继承是什么

什么是Tauri

https://github.com/tauri-apps/tauri/

Tauri 是一个多语言且通用的工具包,非常灵活,允许工程师开发各种类型的应用。它用于构建桌面应用程序,结合了 Rust 工具和 Webview 中渲染的 HTML。使用 Tauri 构建的应用可以包含任意数量的可选 JS API/Rust API,以便 Webview 可以通过消息传递控制系统。实际上,开发者可以扩展默认 API 以添加自己的功能,并轻松地桥接 Webview 和基于 Rust 的后端。

Tauri 应用中的用户界面目前利用 tao 作为 macOS、Windows、Linux、Android 和 iOS 的窗口处理库。为了渲染应用,Tauri 使用 WRY 库,该库提供了一致接口以系统 WebView,在 macOS & iOS 上利用 WKWebView,在 Windows 上利用 WebView2,在 Linux 和 Android 上利用 WebKitGTK,在 Android 上利用 Android System WebView。

Tauri框架流程大致是 Tauri → Wry桥 → Rust

进行分析

跟进查看TauriActivity,我们可以清晰的看到他导入了两个tauri框架的包,他是 Tauri-Android 应用生成的壳 Activity。它把系统的生命周期事件简单地“转发”给 Tauri/Capacitor 的插件体系,让每个插件都有机会同步自己的状态。同时发现它又继承了WryActivity 类。

WryActivity 本身几乎不含业务逻辑;它的作为“桥”,加载了native类,协调在Tauri框架下java层 和native层之间的运行传输,我们需要去跟进查看ez_android_lib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import android.annotation.SuppressLint;
import android.content.pm.PackageInfo;
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
import android.webkit.WebView;
import p058e.AbstractActivityC0854g;
import p087o1.AbstractC1272d;

/* loaded from: classes.dex *///AbstractActivityC0854g是安卓运行的一些逻辑和加密无关
public abstract class WryActivity extends AbstractActivityC0854g {

/* renamed from: y */
public RustWebView f3224y;//y —— 运行期真正承载前端页面的 RustWebView(Tauri 用 Rust-wry 封装的 WebView)。

public static final class Companion {
}

static {
System.loadLibrary("ez_android_lib");//加载native层
}


//这些 native 接口几乎一一对应 Android 生命周期,作用是把 Java 侧的事件转发到Rust侧,让 Rust 负责真正的业务逻辑、状态管理、资源清理等。
private final native void create(WryActivity wryActivity);

private final native void destroy();

private final native void focus(boolean z2);

private final native void memory();

private final native void onActivityDestroy();

private final native void pause();

private final native void resume();

private final native void save();

private final native void start();

private final native void stop();


//getAppClass(name)给 Rust/JS 侧提供一个反射辅助,让 Java 侧反射找类
public final Class<?> getAppClass(String str) {
AbstractC1272d.m3042d("name", str);
return Class.forName(str);
}

//getVersion() —— 获取系统 WebView/Chrome 版本
@SuppressLint({"WebViewApiAvailability", "ObsoleteSdkInt"})
public final String getVersion() {
PackageInfo currentWebViewPackage;
if (Build.VERSION.SDK_INT >= 26) {
currentWebViewPackage = WebView.getCurrentWebViewPackage();
String str = currentWebViewPackage != null ? currentWebViewPackage.versionName : null;
return str == null ? "" : str;
}
try {
return getPackageManager().getPackageInfo("com.android.chrome", 0).versionName.toString();
} catch (Exception e2) {
AbstractC1272d.m3042d("message", "Unable to get package info for 'com.android.chrome'" + e2);
try {
return getPackageManager().getPackageInfo("com.android.webview", 0).versionName.toString();
} catch (Exception e3) {
AbstractC1272d.m3042d("message", "Unable to get package info for 'com.android.webview'" + e3);
return "";
}
}
}

@Override // p058e.AbstractActivityC0854g, androidx.activity.AbstractActivityC0519g, p106v.AbstractActivityC1485h, android.app.Activity
public final void onCreate(Bundle bundle) {
super.onCreate(bundle);
create(this);
}

@Override // p058e.AbstractActivityC0854g, android.app.Activity
public final void onDestroy() {
super.onDestroy();
destroy();
onActivityDestroy();
}

@Override // p058e.AbstractActivityC0854g, android.app.Activity, android.view.KeyEvent.Callback
public final boolean onKeyDown(int i2, KeyEvent keyEvent) {
if (i2 == 4) {
RustWebView rustWebView = this.f3224y;
if (rustWebView == null) {
AbstractC1272d.m3045g("mWebView");
throw null;
}
if (rustWebView.canGoBack()) {
RustWebView rustWebView2 = this.f3224y;
if (rustWebView2 != null) {
rustWebView2.goBack();
return true;
}
AbstractC1272d.m3045g("mWebView");
throw null;
}
}
return super.onKeyDown(i2, keyEvent);
}

@Override // p058e.AbstractActivityC0854g, android.app.Activity, android.content.ComponentCallbacks
public final void onLowMemory() {
super.onLowMemory();
memory();
}

@Override // p058e.AbstractActivityC0854g, android.app.Activity
public void onPause() {
super.onPause();
pause();
}

@Override // p058e.AbstractActivityC0854g, android.app.Activity
public void onResume() {
super.onResume();
resume();
}

@Override // androidx.activity.AbstractActivityC0519g, p106v.AbstractActivityC1485h, android.app.Activity
public final void onSaveInstanceState(Bundle bundle) {
AbstractC1272d.m3042d("outState", bundle);
super.onSaveInstanceState(bundle);
save();
}

@Override // p058e.AbstractActivityC0854g, android.app.Activity
public final void onStart() {
super.onStart();
start();
}

@Override // p058e.AbstractActivityC0854g, android.app.Activity
public final void onStop() {
super.onStop();
stop();
}

@Override // android.app.Activity, android.view.Window.Callback
public final void onWindowFocusChanged(boolean z2) {
super.onWindowFocusChanged(z2);
focus(z2);
}
//供 Rust 侧/插件 在创建完 WebView 后,回传到 Java 活动里保存引用,以便后续拦截键、销毁等。
public final void setWebView(RustWebView rustWebView) {
AbstractC1272d.m3042d("webView", rustWebView);
this.f3224y = rustWebView;
}
}
Android 回调 Java 侧动作 JNI 调用
onCreate create(this) 初始化 Rust 侧 runtime,传递 Activity 指针
onStart start() 页面真正 visible,Rust 可拉起前端逻辑
onResume resume() 恢复渲染/JS 通道,可能重启 WebSocket、计时器
onWindowFocusChanged focus(hasFocus) 通知焦点变化,决定是否拦截键盘、鼠标
onPause pause() 暂停 JS 定时器、媒体、Sensor
onStop stop() 进入后台,关闭长连接、MediaSession
onDestroy destroy()onActivityDestroy() 先释放 Rust 侧资源,再做额外收尾
onLowMemory memory() 内存吃紧,让 Rust 清 cache
onSaveInstanceState save() 备份当前页面状态(history、localStorage 等)

native层

就像是上面说的,这里使用了rust构建.so文件

但是查看相应的native方法并没有和加密逻辑有关的东西,

根据这个文档:https://blog.yllhwa.com/2023/05/09/Tauri%20%E6%A1%86%E6%9E%B6%E7%9A%84%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E6%8F%90%E5%8F%96%E6%96%B9%E6%B3%95%E6%8E%A2%E7%A9%B6/

我们知道真正的资源文件已经被brotli 算法进行压缩后再打包。根据博客上的内容找字符串/index.html

这里有很多的/index.html,而我们要找的/index.html是下面有文件内容地址的那个字符串

这里应该是一个结构体,都是以qword形式存在,第一个元素是文件名,第二个是文件名长度,第三个已经被压缩打包的文件内容地址,,第四个是文件内容长度

把内容dump下来写个python脚本进行解包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import brotli
import binascii

# 读取文件内容
with open("dump.txt", "r") as f:
hex_content = f.read().strip()

print(f"十六进制内容长度: {len(hex_content)}")

try:
# 将十六进制字符串转换为字节
binary_content = binascii.unhexlify(hex_content)
print(f"二进制内容长度: {len(binary_content)} 字节")

# 尝试 Brotli 解压缩
try:
decompressed = brotli.decompress(binary_content)
print(f"解压缩成功!大小: {len(decompressed)} 字节")

# 将解压缩的数据写入文件
with open("dump—_unpack", "wb") as f:
f.write(decompressed)
print("解压缩数据已写入 'dump' 文件")

# 如果是文本,尝试显示前面的部分
try:
preview = decompressed[:200].decode('utf-8', errors='ignore')
print(f"\n解压缩内容预览:\n{preview}")
except:
print("\n解压缩的内容似乎是二进制数据")

except brotli.error as e:
print(f"Brotli 解压缩失败: {e}")

except binascii.Error as e:
print(f"十六进制解码失败: {e}")
print("内容可能不是十六进制格式")

# 尝试直接按二进制读取
print("尝试直接按二进制读取文件...")
try:
with open("export_results.txt", "rb") as f:
binary_content = f.read()
print(f"二进制读取成功!大小: {len(binary_content)} 字节")

try:
decompressed = brotli.decompress(binary_content)
print(f"直接二进制 Brotli 解压缩成功!大小: {len(decompressed)} 字节")
with open("dump_direct", "wb") as f:
f.write(decompressed)
except brotli.error as e:
print(f"直接二进制 Brotli 解压缩失败: {e}")

except Exception as e:
print(f"二进制读取失败: {e}")

print("\n分析完成。请检查生成的 dump 文件获取结果。")

index.html

分析dump下来的html文件,我们发现还要解包一个index-BsFf5qny.js的文件,继续dump+解包+格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + Vue + Typescript App</title>

<script type="module" crossorigin src="/assets/index-BsFf5qny.js"></script>

<link rel="stylesheet" crossorigin href="/assets/index-CJgrqWFO.css">
</head>

<body>
<div id="app"></div>

</body>

</html>

index-BsFf5qny.js

由于解包后的文本比较难阅读,我们可以进行格式化然后分析,分析后实际这个js只是一个瘦前端,功能极简。真正的解题/破解重点在 **Rust 后端命令 **greet 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ul = Ci({
__name: "App",
setup(e) {
const t = Qs(""),
s = Qs("");
async function n() {
t.value = await fl("greet", {
flag: s.value
})
}
return (r, i) => (go(), bo("main", cl, [i[2] || (i[2] = Be("h1", null, "L3HCTF", -1)), Be("form", {
class: "row",
onSubmit: sl(n, ["prevent"])
}, [Si(Be("input", {
id: "greet-input",
"onUpdate:modelValue": i[0] || (i[0] = o => s.value = o),
placeholder: "Enter a flag..."
}, null, 512), [
[ko, s.value]
]), i[1] || (i[1] = Be("button", {
type: "submit"
}, "Validate", -1))], 32), Be("p", null, Fn(t.value), 1)]))
}
});

greet

该加密流程以 14 字节密钥表为核心,对 27 字节输入逐字节执行“表值异或 → 表值相加 → 受表值控制的 8 位循环位移 → 再异或表值”四步混淆,最终让结果与预置的 3 个 64‑位魔数及第 19 字节比对,通过即视为密钥正确。
表值就是dGhpc2lzYWtleQ
最后三个字节是

丢给ai能直接给出脚本,但是要注意最后三个字节的密文需要自己自行推理推理后是(\x4F\x32\x2A)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
S = b"dGhpc2lzYWtleQ"

MAGIC = (
0x0A409663A025150C.to_bytes(8, 'little') +
0x1FE106294065165C.to_bytes(8, 'little') +
0xFC020A4C0E2C7290.to_bytes(8, 'little') +
b'\x4F\x32\x2A' # 末尾 "O2*"
)

def rotr8(x, r):
return ((x >> r) | (x << (8 - r))) & 0xFF

def decrypt(buf: bytearray):
for i in reversed(range(27)):
idx1 = (2 * i + 1) % 14
r = S[(i + 3) % 14] & 7
tmp = buf[i] ^ S[(i + 4) % 14]
tmp = rotr8(tmp, r)
buf[i] = (tmp - S[idx1]) & 0xFF ^ S[i % 14]
return buf

plain = decrypt(bytearray(MAGIC))
print("十六进制:", plain.hex())
print("ASCII :", plain.decode('ascii'))

速通结局:

很巧合的你搜索了wrong,你看到了为wrong_answer,直接跟进到了逻辑主要函数,然后直接ai一把梭